// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.settings

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.database.sqlite.SQLiteConstraintException
import android.database.sqlite.SQLiteDatabase
import android.util.Log
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.edit
import androidx.core.database.sqlite.transaction
import androidx.preference.PreferenceManager
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
import org.apache.commons.io.input.BoundedInputStream
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.data.settings.SettingsRepository
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.net.URL
import java.time.Instant
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream

// FIXME doesn't work on older versions of Android
class DownloadCitiesWorker(appContext: Context, workerParams: WorkerParameters) :
	Worker(appContext, workerParams) {

	companion object {
		const val LAST_UPDATE_KEY = "cities_last_update"
		const val NOTIFICATION_CHANNEL = "cities_channel"
		const val DATABASE_NAME = "geocoding"
		const val ETAG_HEADER_NAME = "ETag"
		const val ETAG_KEY = "cities_etag"
		const val RESULT_ZIP_FILE = "cities.zip"
		const val CITIES_URL = "https://download.geonames.org/export/dump/cities15000.zip"
		const val CITIES_FILE = "cities15000.txt"
		fun shouldUpdate(context: Context): Boolean {
			val (updatesEnabled, weekPassed) = arrayOf(
				SettingsRepository().getGeocoding(context)?.autoUpdate == true,
				Instant.ofEpochSecond(
					PreferenceManager.getDefaultSharedPreferences(context).getLong(LAST_UPDATE_KEY, 0)).plus(7, ChronoUnit.DAYS)
					.isBefore(Instant.now())
			)
			return updatesEnabled && weekPassed
		}

		fun isWorkScheduled(context: Context, name: String): Boolean {
			val instance = WorkManager.getInstance(context)
			val statuses = instance.getWorkInfosForUniqueWork(name)
			try {
				var running = false
				val workInfoList = statuses.get()
				for (workInfo in workInfoList) {
					val state: WorkInfo.State = workInfo.state
					running = (state == WorkInfo.State.RUNNING) or (state == WorkInfo.State.ENQUEUED)
				}
				return running
			} catch (e: ExecutionException) {
				e.printStackTrace()
				return false
			} catch (e: InterruptedException) {
				e.printStackTrace()
				return false
			}
		}
	}

	override fun doWork(): Result {
		val notificationBuilder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL)
			.setSmallIcon(R.drawable.geocoding)
			.setContentTitle(applicationContext.getString(R.string.updating_geocoding_data))
			.setContentText(applicationContext.getString(R.string.downloading_cities_list))
			.setPriority(NotificationCompat.PRIORITY_LOW)
			.setProgress(100, 0, true)
		try {
			if (ActivityCompat.checkSelfPermission(
					applicationContext,
					Manifest.permission.POST_NOTIFICATIONS
				) == PackageManager.PERMISSION_GRANTED
			) {
				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
			}

			val db = SQLiteDatabase.openOrCreateDatabase(
				applicationContext.getDatabasePath(DATABASE_NAME).path,
				null
			)
			val url = URL(CITIES_URL)
			val connection = url.openConnection()
			var length = connection.contentLength.toLong()
			val connectionEtag = connection.getHeaderField(ETAG_HEADER_NAME)
			val savedEtag = PreferenceManager.getDefaultSharedPreferences(applicationContext)
				.getString(ETAG_KEY, null)
			if (savedEtag != null && savedEtag == connectionEtag) {
				if (ActivityCompat.checkSelfPermission(
						applicationContext,
						Manifest.permission.POST_NOTIFICATIONS
					) == PackageManager.PERMISSION_GRANTED
				) {
					NotificationManagerCompat.from(applicationContext).cancel(0)
				}
				Toast.makeText(applicationContext, R.string.cities_list_uptodate, Toast.LENGTH_LONG).show()
				return Result.success()
			}

			db.execSQL("drop table if exists place_names2")
			db.execSQL("drop table if exists places2")
			db.execSQL("create table places2(id text primary key, lat real, lon real)")
			db.execSQL("create table place_names2(id text references places(id), name text primary key)")

			var countingStream =
				BoundedInputStream.Builder()
					.setInputStream(BufferedInputStream(connection.getInputStream())).get()
			val zipFileStream = BufferedOutputStream(
				File(
					applicationContext.noBackupFilesDir.path,
					RESULT_ZIP_FILE
				).outputStream()
			)

			val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
			var bytes = countingStream.read(buffer)
			while (bytes >= 0) {
				zipFileStream.write(buffer, 0, bytes)
				Log.i(
					"geocoding",
					"zip_download: downloaded ${countingStream.count}/$length: ${countingStream.count.toFloat() / length * 100}%"
				)
				if (ActivityCompat.checkSelfPermission(
						applicationContext,
						Manifest.permission.POST_NOTIFICATIONS
					) == PackageManager.PERMISSION_GRANTED
				) {
					notificationBuilder
						.setProgress(100, (countingStream.count.toFloat() / length * 100).toInt(), false)
					NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
				}
				bytes = countingStream.read(buffer)
			}
			countingStream.close()
			zipFileStream.close()

			notificationBuilder
				.setProgress(100, 0, true)
				.setContentText(applicationContext.getString(R.string.saving_cities_list))
			if (ActivityCompat.checkSelfPermission(
					applicationContext,
					Manifest.permission.POST_NOTIFICATIONS
				) == PackageManager.PERMISSION_GRANTED
			) {
				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
			}
			val zipFile = File(applicationContext.noBackupFilesDir.path, RESULT_ZIP_FILE)
			length = zipFile.length()
			countingStream =
				BoundedInputStream.Builder().setInputStream(BufferedInputStream(zipFile.inputStream()))
					.get()
			val stream = ZipInputStream(countingStream)
			var entry: ZipEntry? = stream.nextEntry
			while (entry != null) {
				if (entry.name != CITIES_FILE) {
					entry = stream.nextEntry
					continue
				}
				var count = 0
				db.transaction {
					csvReader { delimiter = '\t' }.open(stream) {
						readAllAsSequence().forEach { row ->
							val names = if (row[3] == "") {
								"${row[1]},${row[2]}"
							} else {
								row[3]
							}
							if (count % 1000 == 0) {
								Log.i(
									"geocoding",
									"${countingStream.count}/$length=${countingStream.count.toFloat() / length * 100}% $names"
								)
								if (ActivityCompat.checkSelfPermission(
										applicationContext,
										Manifest.permission.POST_NOTIFICATIONS
									) == PackageManager.PERMISSION_GRANTED
								) {
									notificationBuilder
										.setProgress(
											100,
											(countingStream.count.toFloat() / length * 100).toInt(),
											false
										)
									NotificationManagerCompat.from(applicationContext)
										.notify(0, notificationBuilder.build())
								}
							}
							count++

							val id = UUID.randomUUID()
							db.execSQL("insert into places2 values(?, ?, ?)", arrayOf(id, row[4], row[5]))
							names.split(",").toSet().forEach { name ->
								try {
									db.execSQL(
										"insert into place_names2 values(?, ?)",
										arrayOf(id, name)
									)
								} catch (e: SQLiteConstraintException) {
									// XXX `on conflict` doesn't work on older versions of Android
									if (e.message?.contains("UNIQUE constraint failed: place_names2.name") != true) {
										throw e
									}
								}
								try {
									db.execSQL(
										"insert into place_names2 values(?, ?)",
										arrayOf(id, "$name, ${row[8]}")
									)
								} catch (e: SQLiteConstraintException) {
									// XXX `on conflict` doesn't work on older versions of Android
									if (e.message?.contains("UNIQUE constraint failed: place_names2.name") != true) {
										throw e
									}
								}
							}
						}
					}
				}
				Log.i("geocoding", "COMPLETE")
				break
			}
			stream.close()
			zipFile.delete()

			db.execSQL("drop index if exists place_names__name")
			db.execSQL("drop table if exists place_names")
			db.execSQL("drop table if exists places")
			db.execSQL("alter table places2 rename to places")
			db.execSQL("alter table place_names2 rename to place_names")
			db.execSQL("create unique index place_names__name on place_names(name)")

			PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
				putLong(LAST_UPDATE_KEY, ZonedDateTime.now().toEpochSecond())
				putString(ETAG_KEY, connectionEtag)
			}

			db.close()
			if (ActivityCompat.checkSelfPermission(
					applicationContext,
					Manifest.permission.POST_NOTIFICATIONS
				) == PackageManager.PERMISSION_GRANTED
			) {
				notificationBuilder
					.setContentText("")
					.setContentTitle(applicationContext.getString(R.string.finished_updating_geocoding_data))
					.setProgress(100, 100, false)
				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
			}
			return Result.success()
		} catch (e: Exception) {
			e.printStackTrace()
			if (ActivityCompat.checkSelfPermission(
					applicationContext,
					Manifest.permission.POST_NOTIFICATIONS
				) == PackageManager.PERMISSION_GRANTED
			) {
				notificationBuilder
					.setContentText("")
					.setContentTitle(applicationContext.getString(R.string.updating_geocoding_data_failed))
					.setProgress(100, 100, false)
				NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build())
			}
			return Result.failure()
		}
	}
}